/*******************************************************************************
 * Copyright (c) 2000, 2003 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials 
 * are made available under the terms of the Common Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/cpl-v10.html
 * 
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.aseclipse.core.runtime.registry.classes {

import flash.events.Event;
import flash.events.EventDispatcher;
import flash.events.IEventDispatcher;
import flash.utils.Dictionary;

import mx.collections.ArrayCollection;

import org.aseclipse.core.runtime.registry.IExtension;
import org.aseclipse.core.runtime.registry.IExtensionPoint;
import org.aseclipse.core.runtime.registry.IExtensionRegistry;

/**
 * An OSGi-free implementation for the extension registry API.
 */
public class ExtensionRegistry extends RegistryModelObject implements IExtensionRegistry, IEventDispatcher {

	//private static final var OPTION_DEBUG_EVENTS_EXTENSION:String = "org.eclipse.core.runtime/registry/debug/events/extension"; //$NON-NLS-1$	
	public static var DEBUG:Boolean;
    private var eventDispatcher:IEventDispatcher;
	// an id->element (IRegistryElement) mapping
	private var elements:Dictionary = new Dictionary();
	// all registry change listeners
	private var listeners:Dictionary = new Dictionary();
	// deltas not broadcasted yet
	private var deltas:Dictionary = new Dictionary();
	// extensions without extension point
	private var orphanExtensions:Dictionary = new Dictionary();
	private var orphanFragments:Dictionary = new Dictionary();
	private var linker:IExtensionLinker;
	//private var reader:RegistryCacheReader = null;
	private var isDirty_:Boolean = false;

	public function ExtensionRegistry(extensionLinker:IExtensionLinker) {
		linker = extensionLinker;
		eventDispatcher = new EventDispatcher();
		/* String debugOption = InternalPlatform.getDefault().getOption(OPTION_DEBUG_EVENTS_EXTENSION);
		DEBUG = debugOption == null ? false : debugOption.equalsIgnoreCase("true"); //$NON-NLS-1$		
		if (DEBUG)
			addRegistryChangeListener(new IRegistryChangeListener() {
			public void registryChanged(IRegistryChangeEvent event) {
				System.out.println(event);
			}
		}); */
	}
	
    /*****
    * Synchronization
    */ 
	public function addArray(regElements:Array):void { // IRegistryElement[] 
		var task:Object = {method:"addArrayTask", elements: regElements};
		putQueue(task);
	}
	
	private function addArrayTask(context:Object):void {
	    isDirty_ = true;
	    var array:Array = context.elements as Array;
	    for (var i:int = 0; i < elements.length; i++)
			basicAdd(elements[i], true);
		fireRegistryChangeEvent();
	}
	
	/**
	 * Adds and resolves all extensions and extension points provided by the
	 * plug-in. Synchronization
	 * <p>
	 * A corresponding IRegistryChangeEvent will be broadcast to all listeners
	 * interested on changes in the given plug-in.
	 * </p>
	 */
	public function add(regElement:IRegistryElement):void {
	    var task:Object = {method:"addTask", element:regElement};
		putQueue(task);
	}
	
	public function addTask(context:Object):void {
	    isDirty_ = true;
		basicAdd(context.element as IRegistryElement, true);
		fireRegistryChangeEvent();
	}

	protected function basicAdd(element:IRegistryElement, link:Boolean):void {
		//TODO: this should be logged
		if (elements[element.getUniqueIdentifier()] != null) {
			trace("********* Element already added: " + element.getUniqueIdentifier() + " - ignored."); //$NON-NLS-1$//$NON-NLS-2$
			return;
		}
		elements[element.getUniqueIdentifier()] = element;
		(element as BundleModel).setParent(this);
		if (!link)
			return;
		if (element.isFragment()) {
			addFragmentTo(element.getUniqueIdentifier(), element.getHostIdentifier());
			// if the master is not present yet, don't add anything
			if (elements[element.getHostIdentifier()] == null)
				return;
		} else {
			var fragmentNames:Array = getFragmentNames(element.getUniqueIdentifier());
			for each(var item:Object in fragmentNames) {
				var fragment:IRegistryElement = elements[item] as IRegistryElement;
				addExtensionsAndExtensionPoints(fragment);
			}
		}
		addExtensionsAndExtensionPoints(element);
	}
	private function addExtensionsAndExtensionPoints(element:IRegistryElement):void {
		// now add and resolve extensions and extension points
		var extPoints:Array = element.getExtensionPoints();
		for each(var item:IExtensionPoint in extPoints)
			this.addExtensionPoint(item);
		var extensions:Array = element.getExtensions();
		for each(var ite:IExtension in extensions)
			this.addExtension(ite);
	}
	/* Utility method to help with array concatenations */

	/*
	 * Creates an association between a fragment and a master element.
	 */
	private function addFragmentTo(fragmentName:String, masterName:String):void {
		var fragmentNames:Array = this.orphanFragments[masterName] as Array;
		if (fragmentNames == null) {
			fragmentNames = new Array();
			orphanFragments[masterName] = fragmentNames;
		}
		fragmentNames.push(fragmentName);
	}
	/*
	 * Removes an association between a fragment and a master element.
	 */
	private function removeFragmentFrom(fragmentName:String, masterName:String):void {
		var fragmentNames:Array = this.orphanFragments[masterName] as Array;
		if (fragmentNames == null)
			return;
		var col:ArrayCollection = new ArrayCollection(fragmentNames);
		for (var i:int=0;i<col.length; i++) {
		    if(fragmentName == col.getItemAt(i) as String) {
		        col.removeItemAt(i);
		        break;
		    }
		}
		if (fragmentNames.length == 0)
			delete orphanFragments[masterName];
	}
	/*
	 * Returns a collection of fragments for a master element.
	 */
	private function getFragmentNames(masterName:String):Array {
		var fragmentNames:Array = orphanFragments[masterName] as Array;
		return fragmentNames == null ? new Array() : fragmentNames;
	}
	private function addExtension(extension:IExtension):void {
		var extPoint:IExtensionPoint = getExtensionPoint(extension.getExtensionPointIdentifier());
		//orphan extension
		if (extPoint == null) {
			// are there any other orphan extensions
			var existingOrphanExtensions:Array = orphanExtensions[extension.getExtensionPointIdentifier()];
			if (existingOrphanExtensions == null) {
			    existingOrphanExtensions = new Array();
			} 
			existingOrphanExtensions.push(extension);	
			return;
		}
		// otherwise, link them
		var existingExtensions:Array = extPoint.getExtensions();
		if (existingExtensions == null) {
		    existingExtensions = new Array();
		} 
		existingExtensions.push(extension);
		linker.link(extPoint, existingExtensions);
		recordChange(extPoint, extension, ExtensionDelta.ADDED);
	}
	/**
	 * Looks for existing orphan extensions to connect to the given extension
	 * point. If none is found, there is nothing to do. Otherwise, link them.
	 */
	private function addExtensionPoint(extPoint:IExtensionPoint):void {
	    var key:String = extPoint.getUniqueIdentifier();
		var existingExtensions:Array = orphanExtensions[key] as Array;
		if (existingExtensions == null) {
			return;
		}
		delete orphanExtensions[key];
		// otherwise, link them
		linker.link(extPoint, existingExtensions);
		recordChanges(extPoint, existingExtensions, ExtensionDelta.ADDED);
	}
	/**
	 * Adds the given listener for registry change events on the given plug-in.
	 */
	/* public function addRegistryChangeListener(listener:IRegistryChangeListener, filter:String):void {
		this.listeners.put(listener, filter);
	} */
	/**
	 * Adds the given listener for registry change events.
	 */
	/* public function addRegistryChangeListener(listener:IRegistryChangeListener):void {
		this.listeners.put(listener, null);
	} */
	/**
	 * Broadcasts the event to all interested parties.
	 */
	private function fireRegistryChangeEvent():void {
		// if there is nothing to say, just bail out
		//if (deltas.isEmpty() || listeners.isEmpty())
		//	return;
		// for thread safety, create tmp collections
		/* Map tmpListeners = new HashMap(listeners);
		Map tmpDeltas = new HashMap(this.deltas);
		// the deltas have been saved for notification - we can clear them now
		deltas.clear();
		// do the notification asynchronously
		new ExtensionEventDispatcherJob(tmpListeners, tmpDeltas).schedule(); */
		//this.dispatchEvent(new RegistryChangeEvent(RegistryChangeEvent.REGISTRY_CHANGE, null));
	}
	
	private function getDelta(elementName:String):RegistryDelta {
		// is there a delta for the plug-in?
		var existingDelta:RegistryDelta = deltas[elementName] as RegistryDelta;
		if (existingDelta != null)
			return existingDelta;
		//if not, create one
		var delta:RegistryDelta = new RegistryDelta(elementName);
		deltas[elementName] = delta;
		return delta;
	}
	
	public function getElementIdentifiers():Array {
	    var array:Array = new Array();
	    for (var key:String in elements) {
	        array.push(key);
	    }
		return array;
	}
	
	public function getConfigurationElementsFor(extensionPointId:String):Array {
	    var array:Array = splitExtensionPointId(extensionPointId);
	    if(array.length == 0)
	        return array;
		return getConfigurationElementsFor2(array[0], array[1]);
	}
	
	public function getConfigurationElementsFor2(pluginId:String, extensionPointSimpleId:String):Array {
		var extPoint:IExtensionPoint = this.getExtensionPointByName(pluginId, extensionPointSimpleId);
		if (extPoint == null)
			return new Array();
		var extensions:Array = extPoint.getExtensions();
		if (extensions.length == 0)
			return new Array();
		var result:Array = new Array();
		for (var i:int = 0; i < extensions.length; i++) {
			var toAdd:Array = (extensions[i] as IExtension).getConfigurationElements();
			for (var j:int = 0; j < toAdd.length; j++)
				result.add(toAdd[j]);
		}
		return result;
	}
	
	public function getConfigurationElementsFor3(pluginId:String, extensionPointName:String, extensionId:String):Array {
		var extension:IExtension = this.getExtension2(pluginId, extensionPointName, extensionId);
		if (extension == null)
			return new Array();
		return extension.getConfigurationElements();
	}
	
	public function getExtensions(elementName:String):Array {
		var element:IRegistryElement = elements[elementName] as IRegistryElement;
		if (element == null)
			return new Array;
		var fragmentNames:Array = getFragmentNames(elementName);
		var allExtensions:ArrayCollection = new ArrayCollection(element.getExtensions());
		for each(var key:String in fragmentNames) {
			var fragment:IRegistryElement = elements[key] as IRegistryElement;
			var fragExtensions:Array = fragment.getExtensions();
			for each(var item:Extension in fragExtensions) {
			    allExtensions.addItem(item);
			}
		}
		return allExtensions.toArray();
	}
	
	/******
	 * @param elementName, qualified plugin id such like "org.eclipse.ui"
	 * @param extensionPointName, simple extension point name like "navigator"
	 */ 
	public function getExtensionsByPoint(elementName:String, pointSimpleName:String):Array {
		var extPoint:IExtensionPoint = getExtensionPointByName(elementName, pointSimpleName);
		if (extPoint != null)
			return extPoint.getExtensions();
		return null;
	}
	/******
	 * @param extensionPointId, qualified extension point name like "org.eclipse.ui.navigator"
	 * @param extensionId, qualified extension id like "org.eclipse.ui.window.content"
	 */ 
	public function getExtension(extensionPointId:String, extensionId:String):IExtension {
		var array:Array = splitExtensionPointId(extensionPointId);
		if(array.length == 0)
		    return null;
		return getExtension2(array[0], array[1], extensionId);
	}
	/******
	 * @param elementId, qualified plugin id such like "org.eclipse.ui"
	 * @param extensionPointName, simple extension point name like "navigator"
	 * @param extensionId, qualified extension id like "org.eclipse.ui.window.content"
	 */ 
	public function getExtension2(elementId:String, 
		extensionPointName:String, extensionId:String):IExtension{
		var extPoint:IExtensionPoint = getExtensionPointByName(elementId, extensionPointName);
		if (extPoint != null)
			return extPoint.getExtension(extensionId);
		return null;
	}
	
	public function getExtensionPoints(elementName:String):Array {
		var element:IRegistryElement = elements[elementName] as IRegistryElement;
		if (element == null)
			return new Array();
		var fragmentNames:Array = getFragmentNames(elementName);
		var allExtensionPoints:ArrayCollection = new ArrayCollection(element.getExtensionPoints());
		for each(var key:String in fragmentNames) {
			var fragment:IRegistryElement = elements[key] as IRegistryElement;
			var fragEPs:Array = fragment.getExtensionPoints();
			for each(var item:Extension in fragEPs) {
			    allExtensionPoints.addItem(item);
			}
		}
		return allExtensionPoints.toArray();
	}
	
	public function getAllExtensionPoints():Array {
		var extensionPoints:ArrayCollection = new ArrayCollection();
		for (var key:String in elements) {
			var model:IRegistryElement = elements[key] as IRegistryElement;
			var toAdd:Array = model.getExtensionPoints();
			for each(var item:IExtensionPoint in toAdd)
				extensionPoints.addItem(item);
		}
		return extensionPoints.toArray();
	}
	
	public function getExtensionPoint(extensionPointId:String):IExtensionPoint {
		var array:Array = splitExtensionPointId(extensionPointId);
		if (array.length == 0)
			return null;
		return getExtensionPointByName(array[0], array[1]);
	}
	
	public function getExtensionPointByName(elementName:String, extensionPointName:String):IExtensionPoint {
		var element:IRegistryElement = elements[elementName] as IRegistryElement;
		if (element == null)
			return null;
		var extPoint:IExtensionPoint = element.getExtensionPoint(extensionPointName);
		if (extPoint != null)
			return extPoint;
		var fragmentNames:Array = getFragmentNames(elementName);
		for (var key:String in fragmentNames) {
		    var ele:IRegistryElement = elements[key];
			extPoint = ele.getExtensionPoint(extensionPointName);
			if (extPoint != null)
				return extPoint;
		}
		return null;
	}
	/*
	 * Records an extension addition/removal.
	 */
	private function recordChange(extPoint:IExtensionPoint, extension:IExtension, kind:int):void {
		var extensionDelta:ExtensionDelta = new ExtensionDelta(extPoint, extension, kind);
		getDelta(extPoint.getParentIdentifier()).addExtensionDelta(extensionDelta);
	}
	/*
	 * Records a set of extension additions/removals.
	 */
	private function recordChanges(extPoint:IExtensionPoint, extensions:Array, kind:int):void {
		if (listeners.isEmpty())
			return;
		if (extensions.length == 0)
			return;
		var pluginDelta:RegistryDelta = getDelta(extPoint.getParentIdentifier());
		for (var i:int = 0; i < extensions.length; i++) {
			var delta:ExtensionDelta = new ExtensionDelta(extPoint, extensions[i], kind);
			pluginDelta.addExtensionDelta(delta);
		}
	}
	/**
	 * Unresolves and removes all extensions and extension points provided by
	 * the plug-in. Synchronization
	 * <p>
	 * A corresponding IRegistryChangeEvent will be broadcast to all listeners
	 * interested on changes in the given plug-in.
	 * </p>
	 */
	public function remove(elementName:String, bundleId:int):Boolean { 
	    var task:Object = {method:"removeTask", elementName:elementName, bundleId:bundleId};
		putQueue(task); 
		return true;
	}
	
	public function removeTask(context:Object):void{
		var elementName:String = context.elementName as String; 
		var bundleId:int = context.bundleId as int;
		var element:IRegistryElement = elements[elementName] as IRegistryElement;
		if (element == null) {
			// if (DEBUG)
			trace("********* Element unknown: " + elementName + " - not removed."); //$NON-NLS-1$//$NON-NLS-2$
			return; // false
		}
		if (element.getId() != bundleId)
			return; // bundleId

		isDirty_ = true;
		if (element.isFragment()) {
			// if the master is not present yet, bail out
			if (elements[element.getHostIdentifier()] == null) {
				removeFragmentFrom(elementName, element.getHostIdentifier());
				delete elements[elementName];
				return; // true
			}
		} else {
			var fragmentNames:Array = getFragmentNames(element.getUniqueIdentifier());
			for each(var key:String in fragmentNames) {
				var fragment:IRegistryElement = elements[key] as IRegistryElement;
				removeExtensionsAndExtensionPoints(fragment);
			}
		}
		removeExtensionsAndExtensionPoints(element);
		// remove link between master and fragment
		removeFragmentFrom(elementName, element.getHostIdentifier());

		// remove it in the end
		delete elements[elementName];
		fireRegistryChangeEvent();
		return; // true
	}
	private function removeExtensionsAndExtensionPoints(element:IRegistryElement):void {
		// remove extensions
		var extensions:Array = element.getExtensions();
		for (var i:int = 0; i < extensions.length; i++)
			this.removeExtension(extensions[i]);
		// remove extension points
		var extPoints:Array = element.getExtensionPoints();
		for (var j:int = 0; j < extPoints.length; j++)
			this.removeExtensionPoint(extPoints[j]);
	}
	private function removeExtensionPoint(extPoint:IExtensionPoint):void {
		if (extPoint.getExtensions() != null) {
			var existingExtensions:Array = extPoint.getExtensions();
			orphanExtensions.put(extPoint.getUniqueIdentifier(), existingExtensions);
			linker.link(extPoint, null);
			recordChanges(extPoint, existingExtensions, ExtensionDelta.REMOVED);
		}
	}
	private function removeExtension(extension:IExtension):void {
	    var extensionId:String = extension.getExtensionPointIdentifier();
		var extPoint:IExtensionPoint = getExtensionPoint(extensionId);
		if (extPoint == null) {
			// not found - maybe it was an orphan extension
			var existingOrphanExtensions:Array = orphanExtensions[extensionId];
			if (existingOrphanExtensions == null)
				// nope, this extension is unknown
				return;
			// yes, so just remove it from the orphans list
			removeFromArray(existingOrphanExtensions, extension);
			return;
		}
		// otherwise, unlink the extension from the extension point
		var existingExtensions:Array = extPoint.getExtensions();
		removeFromArray(existingExtensions, extension);
		linker.link(extPoint, existingExtensions);
		recordChange(extPoint, extension, ExtensionDelta.REMOVED);
	}
    
	/**
	 * Adds the given listener for registry change events related to this
	 * plug-in's extension points.
	 */
	/* public function removeRegistryChangeListener(listener:IRegistryChangeListener):void {
		this.listeners.remove(listener);
	} */
    public function removeEventListener(key:String, listener:Function, useCapture:Boolean = false):void {
		eventDispatcher.removeEventListener(key, listener, useCapture);
	}
	public function addEventListener(key:String, listener:Function, useCapture:Boolean = false, priority:int = 0, weekReference:Boolean = false):void {
		eventDispatcher.addEventListener(key, listener, useCapture, priority, weekReference);
	}
	public function dispatchEvent(event:Event):Boolean {
		return eventDispatcher.dispatchEvent(event);
	}
	public function hasEventListener(type:String):Boolean {
		return eventDispatcher.hasEventListener(type);
	}    
	public function willTrigger(type:String):Boolean {
		return eventDispatcher.willTrigger(type);
	}    
	
	/* 
	public class ExtensionEventDispatcherJob extends Job {
		private Map listeners;
		private Map deltas;
		public ExtensionEventDispatcherJob(Map listeners, Map deltas) {
			super("RegistryChangeEventDispatcherJob"); //$NON-NLS-1$
			this.listeners = listeners;
			this.deltas = deltas;
		}
		public IStatus run(IProgressMonitor monitor) {
			MultiStatus result = new MultiStatus(IPlatform.PI_RUNTIME, IStatus.OK, Policy.bind("pluginEvent.errorListener"), null); //$NON-NLS-1$			
			for (Iterator iter = listeners.entrySet().iterator(); iter.hasNext();) {
				Map.Entry entry = (Map.Entry) iter.next();
				IRegistryChangeListener listener = (IRegistryChangeListener) entry.getKey();
				String filter = (String) entry.getValue();
				if (filter != null && !deltas.containsKey(filter))
					continue;
				try {
					listener.registryChanged(new RegistryChangeEvent(deltas, filter));
				} catch (RuntimeException re) {
					String message = re.getMessage() == null ? "" : re.getMessage(); //$NON-NLS-1$
					result.add(new Status(IStatus.ERROR, IPlatform.PI_RUNTIME, IStatus.OK, message, re));
				}
			}
			return result;
		}
	} */
	public function getElement(elementId:String):IRegistryElement {
		return elements[elementId] as IRegistryElement;
	}

	/* public function setCacheReader(value:RegistryCacheReader):void {
		reader = value;
	}
	
	public function getCacheReader():RegistryCacheReader {
		return reader;
	} */

	public function isDirty():Boolean {
		return isDirty_;
	}

	public function setDirty(value:Boolean):void {
		isDirty_ = value;
	}
	
	/***
	 * @param extensionPointId qualified namespace of extension point, like "a.b.c.d"
	 * return array with two elments, like ["a.b.c", "d"]
	 */ 
	private function splitExtensionPointId(extensionPointId:String):Array {
		var lastdot:int = extensionPointId.lastIndexOf('.');
		var result:Array = new Array(2);
		if (lastdot != -1) {
    		result[0] = extensionPointId.substring(0, lastdot);
    		result[1] = extensionPointId.substring(lastdot + 1);
		}
		return result;
	}	
    private function removeFromArray(array:Array, item:Object):void {
		var list:ArrayCollection = new ArrayCollection(array);
		for(var i:int=0;i<list.length;i++) {
		    if (item == list.getItemAt(i)) {
		        list.removeItemAt(i);
		        break;
		    }
		}        
    }	
	/************************************************
	 * Queue to manage the synchronization of addAll, add, and remove operations
	 ************************************************/ 	
	private var stack:Array = new Array();
	private var busyFlag:Boolean = false; // flag to control the synchronization
	
	private function putQueue(task:Object):void {
	    stack.push(task);
	    executeQueue();
	}
	
	private function executeQueue():void {
	    if(stack.length == 0) {
	        return;
	    }
	    if(busyFlag == false) {
	        var task:Object = stack.pop();
	        if(task != null) {
	            busyFlag = true;
	            var method:String = task.method as String;
        	    if(this.hasOwnProperty(method)) {
        	        try {
        	            this[method](task);
        	        } catch(er:Error) { trace(er.getStackTrace());
        	        }
        	        busyFlag = false;
        	    } else {
        	        busyFlag = false; 
        	        trace("Don't have method named '" + method + "' with parameter " + task.toString());
        	    }
	        }
	        executeQueue();
	    }
	}
}
}